package main

/*================================= includes ======================*/
import (
    "fmt"
    "os"
    "bufio"
    "os/exec"
    "io"
    pflag "github.com/spf13/pflag"
)

/*================================= types =========================*/

type selpg_args struct {
    start_pg int; /* start page, must be defined, default -1*/
    end_pg int;/* end page, must be defined, default -1*/
    in_filename string;/*path of the input file, optional*/
    pg_len int; /* default value, can be overriden by "-l number" on command line */
    form_feed bool;/* true if there is -f argument, default value is false*/
    print_dest string;/*path of the output destination, optional*/
}

/*================================= globals =======================*/

var progname string; /* program name, for error messages */

/*================================= main()=== =====================*/

func main() {
    var sa selpg_args
    parseArgs(&sa)
    //DEBUG: 
    //printSa(&sa)
    checkArgs(&sa)
    processInput(&sa)
}

/*================================= init()=== =====================*/
// Initially invoked at the beginning of the program, 
// Even before main
func init() {
    // set program name
    progname = os.Args[0]
    // change the pflage Usage function to self defined usage function
    pflag.Usage = usage
}

/*================================= parseArgs()=== =====================*/
// parse the command line arguments using package pflag
// and store the parsing result in struct selpg_args
func parseArgs(sa *selpg_args){
    // setup mandatory args: -s and -e
    pflag.IntVarP(&(sa.start_pg), "start", "s", -1, "start page")
    pflag.IntVarP(&(sa.end_pg), "end", "e", -1, "end page")
    // setup optional args: -l, -f, -d
    pflag.IntVarP(&(sa.pg_len), "len", "l", 10, "self-defined page length")
    pflag.BoolVarP(&(sa.form_feed), "form_feed", "f", false, "enable ASCII form feed")
    pflag.StringVarP(&(sa.print_dest), "dest", "d", "stdout", "output destination")
    // start parsing
    pflag.Parse()

    // see if there is input file arugment (which is a non-flag argument)
    // flag.Args() or pflag.Args(): list of non-flag arguments
    // (p)flag.Narg(): number of non-flag arguments
    add_args := pflag.Args()
    add_args_cnt := pflag.NArg()

    if add_args_cnt > 0{            // 1. there is non-flag arguments
        sa.in_filename = add_args[0]// take the first one as input file
    } else {                        // 2. there isn't non-flag arguments
        sa.in_filename = ""         // set in_filename as empty string
    }                               // and use Stdin as input file
}

/*================================= checkArgs()=== =====================*/
// Invoked after all arguments parsed into selpg_args
// In order to check if they're valid
func checkArgs(sa *selpg_args) {
    /* Not enough args, minimum command is "selpg -sstartpage -eend_page"  */
    if sa.start_pg == -1 || sa.end_pg == -1 {
        fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", progname)
        usage()
        os.Exit(1)
    }

    /* check mandatory args first */
    /* check 1st arg - start page */
    /* valid: s_pg >= 1 */
    if sa.start_pg < 1 {
        fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname, sa.start_pg)
        usage()
        os.Exit(3)
    }

    /* check 2nd arg - end page */
    /* valid: e_pg > 1 and >= s_pg */
    if sa.end_pg < 1 || sa.end_pg < sa.start_pg {
        fmt.Fprintf(os.Stderr, "%s: invalid start page %d\n", progname, sa.end_pg)
        usage()
        os.Exit(5)
    }

    /* now handle optional args */
    /* check page length */
    //DEBUG:
    //fmt.Println(sa.pg_len)
    /* check: -l=number should larger than 0*/
    if sa.pg_len <= 0 {
        fmt.Fprintf(os.Stderr, "%s: invalid page length %d\n", progname, sa.pg_len)
        usage()
        os.Exit(6)
    }
    /* check: -l and -f should not exist together */
    /* pg_len == 10, which is the default value */
    /* but omission is that -l10 -f could pass the check */
    if sa.pg_len != 10 && sa.form_feed == true {
        fmt.Fprintf(os.Stderr, "%s: -l and -f cannot exist together %d\n", progname, sa.pg_len)
        usage()
        os.Exit(7)
    }
    /* check input file */
    if pflag.NArg() > 0{
        // check if input file exists
        in_file_exists, err := PathExists(sa.in_filename)
        // if it exists, check if it's readable
        if in_file_exists {
            if !fileReadable(sa.in_filename) {
                fmt.Fprintf(os.Stderr, "%s: input file \"%s\" exists but cannot be read\n", progname, sa.in_filename)
                os.Exit(11)
            }
        } else { // if it doesn't exist, then throw an error
            fmt.Fprintf(os.Stderr, "%s: input file \"%s\" does not exist\n", progname, sa.in_filename)
            if err != nil {
                fmt.Fprintln(os.Stderr, err)
            }
            os.Exit(10)
        }
    }
}

/*================================= processInput()========================*/
// setup correct input and output file according to arguments in selpg_args
// after that it would invoke output()
func processInput(sa *selpg_args){
    // 1. setup input file
    var fin io.ReadCloser     // Note: fin could be Stdin or a file
                              // both of them could be described using
                              // a file descriptor: os.File
    var in_err error
    if sa.in_filename == "" { // 1.1 if in_filename not defined
        fin = os.Stdin        // then set Stdin as input
    } else {                  // 1.2 if defined, then set it as input
        fin, in_err = os.OpenFile(sa.in_filename, os.O_RDONLY, 0)
        if in_err != nil {    // if error occurs while opening file, alert
            fmt.Fprintf(os.Stderr, "%s: could not open input file \"%s\"\n", progname, sa.in_filename)
            panic(in_err)
        }
    }
    
    // 2. setup output file
    var fout io.WriteCloser  // Note: fout could be Stdout 
                             // or a Stdin pipe of "lp" command
                             // the Stdin pipe is a io.WriteCloser interface
                             // Stdout happens to be a io.WriteCloser too
                             // Ps: io.WriteCloser means an interface
                             // implements both Write() and Close() methods
    if sa.print_dest == "stdout" {
        fout = os.Stdout
    } else {
        fout = exec_lp(sa)
    }

    // 3. after setting up input and output, transmit the data
    output(fout, fin, sa)
}

/*================================= output()========================*/
// transmits data from @file to @out, which were setup in processInput()
// transmission situation differs when argument "-l" occurs or "-f" occurs
// structure of the function: 
//      initiation
//      for true:
//          if -l
//          if -f
//      handle errors and close
func output(out io.WriteCloser, in io.ReadCloser, sa *selpg_args){
    // 1. initiation: 
    // necessary assist counter: page_counter and line_counter
    var line_ctr, page_ctr int
    page_ctr = 1
    line_ctr = 1
    // reader used to read data from file
    // using package bufio
    reader := bufio.NewReader(in)

    // 2. the main loop
    for ; true; {
        var one_seg string  // a segment of data in reading progress
                            // refers to a page when -f, a line when -l
        var read_err error

        // 2.1 two reading situations : -l and -f
        // -l: read a line (seperate with '\n') each time
        // -f: read a page (seperate with '\n') each time
        if sa.form_feed {
            one_seg, read_err = reader.ReadString('\f')
            page_ctr += 1
        } else {
            one_seg, read_err = reader.ReadString('\n')
            line_ctr += 1
            if line_ctr > sa.pg_len {
                page_ctr += 1
                line_ctr = 1
            }
        }

        // 2.2 handles error happened in reading progress
        // if EOF, it's the end of file -> break
        // if nil -> no error
        // else -> there is an error
        if read_err == io.EOF {
            break
        }
        if read_err != nil {
            fmt.Fprintf(os.Stderr, "%s: error occur while reading file \"%s\"\n", progname, sa.in_filename)
            panic(read_err)
        }

        //DEBUG:
        //fmt.Println(one_seg)
        //fmt.Print("page_ctr: ",page_ctr, "\n")

        // 2.3 Write data to @out
        // if the data segment is between start page and end page
        // then write it to @out
        if (page_ctr >= sa.start_pg) && (page_ctr <= sa.end_pg) {
            //DEBUG:
            //fmt.Println(one_seg)
            //fmt.Print(out)

            //Note: Write() only accept []byte type argument
            _, write_err := out.Write([]byte(one_seg)) 
            if write_err != nil {
                fmt.Fprintf(os.Stderr, "%s: error occur while writing to \"%s\"\n", progname, sa.print_dest)
                panic(write_err)
            }
        }
    }

    // 3. handles situations: no pages to print / not enough pages to print
    if page_ctr < sa.start_pg {
        fmt.Fprintf(os.Stderr, "%s: start_page (%d) greater than total pages (%d),no output written\n", progname, sa.start_pg, page_ctr)
    } else if page_ctr < sa.end_pg {
        fmt.Fprintf(os.Stderr, "%s: end_page (%d) greater than total pages (%d), less output than expected\n", progname, sa.end_pg, page_ctr)
    }
    in.Close()
    out.Close()
    fmt.Fprintf(os.Stderr, "%s: Done\n", progname)
}

/*================================= exec_lp()========================*/
// if -d argument is defined, this function would be invoked
// -d=lp1 passes the selected pages to command "lp -dlp1" as it's input
// so this function gets the Stdin pipe of command "lp"
// and returns the pipe to processInput() as the data output destination
func exec_lp(sa *selpg_args) io.WriteCloser{
    // get a command struct using exec.Command()
    cmd := exec.Command("lp", "-d"+sa.print_dest)
    // get the Stdin Pipe from commmand struct (that's what we want)
    fout, in_pipe_err := cmd.StdinPipe()
    if in_pipe_err != nil {
        fmt.Fprintf(os.Stderr, "%s: could not open pipe to \"%s\"\n", progname, cmd.Path)
        panic(in_pipe_err)
    }
    // setup the correct Stdout pipe and Stderr pipe
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    // start the execution
    start_err := cmd.Start()
    if start_err != nil {
        fmt.Fprintf(os.Stderr, "%s: could not start cmd \"%s\"\n", progname, cmd.Path)
        panic(start_err)
    }
    return fout
}

/*================================= usage()========================*/
// indicates the correct usage of selpg
func usage() {
    fmt.Fprintf(os.Stderr, `USAGE: %s -sstart_page -eend_page [ -f | -llines_per_page ][ -ddest ] [ in_filename ]

Options:
`, progname)
    pflag.PrintDefaults()
}

/*================================= PathExists()========================*/
// check if the path/file exists
func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

/*================================= PathExists()========================*/
// check if the file in @path is readable
func fileReadable(path string) bool {
    file_info, err := os.Stat(path)
    if err != nil {
        fmt.Fprintln(os.Stderr, "os.Stat failed:", err)
        panic(err)
    }

    file_mode := file_info.Mode()
    perm := file_mode.Perm()
    // 292: 000 100 100 100
    flag := perm & os.FileMode(292) 
    if uint32(flag) == uint32(292) {
        return true
    } else {
        return false
    }
}

/*================================= printSa()========================*/
// print the members in struct selpg_args, used for debug
func printSa(sa *selpg_args){
    fmt.Printf("Args: %v\n", pflag.Args())
    fmt.Printf("NArg: %d\n", pflag.NArg())
    fmt.Printf("s: %d\n", sa.start_pg)
    fmt.Printf("e: %d\n", sa.end_pg)
    fmt.Printf("l: %d\n", sa.pg_len)
    fmt.Printf("f: %v\n", sa.form_feed)
    fmt.Printf("in: %s\n", sa.in_filename)
    fmt.Printf("d: %s\n", sa.print_dest)
}